/*
 * Copyright 1999-2011 Alibaba Group.
 *  
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *  
 *      http://www.apache.org/licenses/LICENSE-2.0
 *  
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.alibaba.dubbo.common.bytecode;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.atomic.AtomicLong;

import com.alibaba.dubbo.common.utils.ClassHelper;
import com.alibaba.dubbo.common.utils.ReflectUtils;

/**
 * Mixin
 * 
 * @author qian.lei
 */

public abstract class Mixin
{
	private static AtomicLong MIXIN_CLASS_COUNTER = new AtomicLong(0);

	private static final String PACKAGE_NAME = Mixin.class.getPackage().getName();

	public static interface MixinAware{ void setMixinInstance(Object instance); }

	/**
	 * mixin interface and delegates.
	 * all class must be public.
	 * 
	 * @param ics interface class array.
	 * @param dc delegate class.
	 * @return Mixin instance.
	 */
	public static Mixin mixin(Class<?>[] ics, Class<?> dc)
	{
		return mixin(ics, new Class[]{dc});
	}

	/**
	 * mixin interface and delegates.
	 * all class must be public.
	 * 
	 * @param ics interface class array.
	 * @param dc delegate class.
	 * @param cl class loader.
	 * @return Mixin instance.
	 */
	public static Mixin mixin(Class<?>[] ics, Class<?> dc, ClassLoader cl)
	{
		return mixin(ics, new Class[]{dc}, cl);
	}

	/**
	 * mixin interface and delegates.
	 * all class must be public.
	 * 
	 * @param ics interface class array.
	 * @param dcs delegate class array.
	 * @return Mixin instance.
	 */
	public static Mixin mixin(Class<?>[] ics, Class<?>[] dcs)
	{
		return mixin(ics, dcs, ClassHelper.getCallerClassLoader(Mixin.class));
	}

	/**
	 * mixin interface and delegates.
	 * all class must be public.
	 * 
	 * @param ics interface class array.
	 * @param dcs delegate class array.
	 * @param cl class loader.
	 * @return Mixin instance.
	 */
	public static Mixin mixin(Class<?>[] ics, Class<?>[] dcs, ClassLoader cl)
	{
		assertInterfaceArray(ics);

		long id = MIXIN_CLASS_COUNTER.getAndIncrement();
		String pkg = null;
		ClassGenerator ccp = null, ccm = null;
		try
		{
			ccp = ClassGenerator.newInstance(cl);

			// impl constructor
			StringBuilder code = new StringBuilder();
			for(int i=0;i<dcs.length;i++)
			{
				if( !Modifier.isPublic(dcs[i].getModifiers()) )
				{
					String npkg = dcs[i].getPackage().getName();
					if( pkg == null )
					{
						pkg = npkg;
					}
					else
					{
						if( !pkg.equals(npkg)  )
							throw new IllegalArgumentException("non-public interfaces class from different packages");
					}
				}

				ccp.addField("private " + dcs[i].getName() + " d" + i + ";");

				code.append("d").append(i).append(" = (").append(dcs[i].getName()).append(")$1[").append(i).append("];\n");
				if( MixinAware.class.isAssignableFrom(dcs[i]) )
					code.append("d").append(i).append(".setMixinInstance(this);\n");
			}
			ccp.addConstructor(Modifier.PUBLIC, new Class<?>[]{ Object[].class }, code.toString());

			// impl methods.
			Set<String> worked = new HashSet<String>();
			for(int i=0;i<ics.length;i++)
			{
				if( !Modifier.isPublic(ics[i].getModifiers()) )
				{
					String npkg = ics[i].getPackage().getName();
					if( pkg == null )
					{
						pkg = npkg;
					}
					else
					{
						if( !pkg.equals(npkg)  )
							throw new IllegalArgumentException("non-public delegate class from different packages");
					}
				}

				ccp.addInterface(ics[i]);

				for( Method method : ics[i].getMethods() )
				{
					if( "java.lang.Object".equals(method.getDeclaringClass().getName()) )
						continue;

					String desc = ReflectUtils.getDesc(method);
					if( worked.contains(desc) )
						continue;
					worked.add(desc);

					int ix = findMethod(dcs, desc);
					if( ix < 0 )
						throw new RuntimeException("Missing method [" + desc + "] implement.");

					Class<?> rt = method.getReturnType();
					String mn = method.getName();
					if( Void.TYPE.equals(rt) )
						ccp.addMethod(mn, method.getModifiers(), rt, method.getParameterTypes(), method.getExceptionTypes(),
								"d" + ix + "." + mn + "($$);");
					else
						ccp.addMethod(mn, method.getModifiers(), rt, method.getParameterTypes(), method.getExceptionTypes(),
								"return ($r)d" + ix + "." + mn + "($$);");
				}
			}

			if( pkg == null )
				pkg = PACKAGE_NAME;

			// create MixinInstance class.
			String micn = pkg + ".mixin" + id;
			ccp.setClassName(micn);
			ccp.toClass();

			// create Mixin class.
			String fcn = Mixin.class.getName() + id;
			ccm = ClassGenerator.newInstance(cl);
			ccm.setClassName(fcn);
			ccm.addDefaultConstructor();
			ccm.setSuperClass(Mixin.class.getName());
			ccm.addMethod("public Object newInstance(Object[] delegates){ return new " + micn + "($1); }");
			Class<?> mixin = ccm.toClass();
			return (Mixin)mixin.newInstance();
		}
		catch(RuntimeException e)
		{
			throw e;
		}
		catch(Exception e)
		{
			throw new RuntimeException(e.getMessage(), e);
		}
		finally
		{
			// release ClassGenerator
			if( ccp != null )
				ccp.release();
			if( ccm != null )
				ccm.release();
		}
	}

	/**
	 * new Mixin instance.
	 * 
	 * @param ds delegates instance.
	 * @return instance.
	 */
	abstract public Object newInstance(Object[] ds);

	protected Mixin(){}

	private static int findMethod(Class<?>[] dcs, String desc)
	{
		Class<?> cl;
		Method[] methods;
		for(int i=0;i<dcs.length;i++)
		{
			cl = dcs[i];
			methods = cl.getMethods();
			for( Method method : methods )
			{
				if( desc.equals(ReflectUtils.getDesc(method)) )
					return i;
			}
		}
		return -1;
	}

	private static void assertInterfaceArray(Class<?>[] ics)
	{
		for(int i=0;i<ics.length;i++)
			if( !ics[i].isInterface() )
				throw new RuntimeException("Class " + ics[i].getName() + " is not a interface.");
	}
}